import os
import sys
import queue
import signal
import logging
import shutil


from base.build_graph import BuildGraph
from base.defines import Attribute
from base.context import Context
from base.build_pool import BuildPool
from base.defines import CompileType
from base.build_statistics import BuildStatistics
from utils.common_utils import real_file_path, file_md5, str_md5
from utils.db_object_pool import DBObjectPool
from command_parser.option_base import *


class BuildOrganizer(object):
    def __init__(self):
        self.__context = Context()
        self.__build_graph = BuildGraph()
        self.__build_statistics = BuildStatistics()
        self.__work_queue = queue.Queue()
        self.__finished_queue = queue.Queue()
       
        self.__build_pool = BuildPool(finished_queue=self.__finished_queue, build_statistics=self.__build_statistics)
        
        # interprete signal
        self.__raw_int_signal_handler = signal.SIG_DFL
        self.__raw_term_signal_handler = signal.SIG_DFL
        self.__break_signal = signal.SIG_DFL
        self.__predef_macro_dict = {}
        
        self.__ast_dep_dict = {}
        
    def init_build_env(self):
        self.__build_pool.init_build_pool()
        self.__raw_int_signal_handler = signal.signal(signal.SIGINT, self.__signal_handler)
        self.__raw_term_signal_handler = signal.signal(signal.SIGTERM, self.__signal_handler)
        
    def release_build_env(self):
        self.__build_pool.shutdown()
        signal.signal(signal.SIGINT, self.__raw_int_signal_handler)
        signal.signal(signal.SIGINT, self.__raw_term_signal_handler)
        
        
    def __signal_handler(self, signal_num, frame):
        self.__break_signal = signal_num
        self.release_build_env()
        
        
    def __update_build_work_graph(self, reversed_build_graph=None):
        while not self.__finished_queue.empty():
            finished_node = self.__finished_queue.get()
            candidate_nodes = reversed_build_graph.successors(finished_node)
            for node in candidate_nodes:
                degree_dict = reversed_build_graph.node.get(node)
                if not degree_dict:
                    self.__work_queue.put(node)
                    continue
                degree_dict[node] -= 1
                degree = degree_dict.get(node)
                if degree == 0:
                    self.__work_queue.put(node)
                    
    
    def __check_file_exist(self, file="", deps_md5=[], cache_map={}):
        ans_name = ""
        
        if not file:
            return ans_name
        
        f_md5 = file_md5(file)
        if not f_md5 or f_md5 not in deps_md5:
            # file not exists or file is not the original one
            for dep_md5 in deps_md5:
                dep_name = cache_map.get(dep_md5, None)
                if dep_name == file:
                    # find the input file copy
                    cache_name = os.path.join(self.__context.CACHE_DIR, dep_md5+self.__context.CACHE_EXT)
                    if os.path.exists(cache_name):
                        return cache_name
                    else:
                        logging.debug("Can not find source file %s and it's copy %s" % (file, dep_md5))
                        return ans_name
            logging.warning("Can not find source file %s and it's copy %s" % (file, dep_md5))
            return ans_name
        
        else:
            # file is match the used one
            return file
        
    def __replace_link_input_options(self, dst_md5="", raw_input_opts=[], dependencies=[]):
        # the raw_input_options should correspond to dependencies, as for as would modify itself archive, we need add the original archive as input file
        index = 0
        deps_src_md5 = [] 
        if len(raw_input_opts) < len(dependencies):
            for x in range(len(dependencies) - len(raw_input_opts)):
                opt = Option(type=PREFIX_SIMPLE, keep=False)
                raw_input_opts.append(opt)
            
        for src_md5 in dependencies:
            src_deps_md5 = self.__ast_dep_dict.get(src_md5, [])
            if not src_deps_md5:
                # it mast a single file, so we test it existence
                ast_path = os.path.join(self.__context.AST_DIR, src_md5+self.__context.AST_EXT)
                maple_path = os.path.join(self.__context.MAPLE_DIR, src_md5+self.__context.MAPLE_EXT)
                # if not, it indicates ast to mpl failed
                if os.path.exists(maple_path):
                    if self.__context.IR_CAT_MODE:
                        self.__ast_dep_dict[src_md5] = [maple_path]
                        src_deps_md5 = [maple_path]
                    else:
                        self.__ast_dep_dict[src_md5] = [ast_path]
                        src_deps_md5 = [ast_path]

            deps_src_md5.extend(src_deps_md5)
            if src_deps_md5:
                raw_input_opts[index].parameter = src_deps_md5
                raw_input_opts[index].keep = True
            index += 1
        
        self.__ast_dep_dict[dst_md5] = deps_src_md5
        if not deps_src_md5:
            # lock of all inputs files, so we should skip the node
            logging.warning("Lack of all input files when build node %s" % dst_md5)
        return len(deps_src_md5)
    
    def __replace_IO_options(self, parser=None, dst_md5=""):
        if not parser or not dst_md5:
            raise Exception("It occurs failure when replace input/output options")
        
        compile_type = self.__build_graph.read_attribute(node=dst_md5, attribute=Attribute.COMPILER)
        db_handle = self.__build_graph.read_attribute(node=dst_md5, attribute=Attribute.DB_HANDLER)
        cwd = self.__build_graph.read_attribute(node=dst_md5, attribute=Attribute.CWD)
        name_dep = db_handle.get_name_dep()
        link_dep = db_handle.get_link_dep()
        cache_db_handler = DBObjectPool.get_db_inst_by_name("filecache")
        cache_dep = cache_db_handler.get_cache_map()
        
        if compile_type == CompileType.CLANG or compile_type == CompileType.GCC:
            # compile type command, we just replace output option
            # then we should check if source file exists
            # TODO: we may should provide a general API for replacing the IO option
            ast_path = os.path.join(self.__context.AST_DIR, dst_md5+self.__context.AST_EXT)
            parser.update_output_option(new_output=ast_path)
            
            # for compile, just one input
            input_option = parser.get_input_options()
            input_name = real_file_path(input_option[0].parameter[0], cwd=cwd)
            
            dep_files_md5 = link_dep.get(dst_md5)
            ans_name = self.__check_file_exist(file=input_name, deps_md5=dep_files_md5, cache_map=cache_dep)
            if not ans_name:
                logging.warning("Can not find input file %s by node %s" % (input_name, dst_md5))
                return False
            input_option[0].parameter = [ans_name]
            return True
        
        elif compile_type == CompileType.LD or compile_type == CompileType.AR:
            mpl_path = os.path.join(self.__context.MAPLE_DIR, dst_md5 + self.__context.MAPLE_EXT)
            ori_command = parser.get_original_command()
            parser.update_output_option(new_output=mpl_path)
            
            input_opts = parser.get_input_options()
            
            for opt in input_opts:
                opt.keep = False
                
            dependencies = link_dep.get(dst_md5, None)
            exist_inputs_num = 0
            if dependencies:
                try:
                    exist_inputs_num = self.__replace_link_input_options(dst_md5=dst_md5, raw_input_opts=input_opts, dependencies=dependencies)
                except:
                    logging.error("link command convert error. command: %s" % ori_command)
            if not exist_inputs_num:
                return False
            return True
        
    
    def irbuild(self):
        if not self.__build_graph.is_initized():
            raise Exception("[ERROR] Build Graph is not initized before build ir!")
        
        reversed_graph = self.__build_graph.get_reversed_graph_with_degree()
        
        root_nodes = [node for node, in_degree in reversed_graph.in_degree_iter() if in_degree == 0]
        
        for root_node in root_nodes:
            self.__finished_queue.put(root_node)

        self.__build_statistics.update_total_targets(num=len(reversed_graph.node) - len(root_nodes))
        
        self.__build_statistics.print_progress_bar()
        
        while self.__break_signal == signal.SIG_DFL and not self.__finished_queue.empty():
            self.__update_build_work_graph(reversed_build_graph=reversed_graph)
            while not self.__work_queue.empty():
                work_node = self.__work_queue.get()
                # print("this:", work_node, '\n')
                ori_command = self.__build_graph.read_attribute(node=work_node, attribute=Attribute.COMMAND)
                try:
                    parser = self.__build_graph.read_attribute(node=work_node, attribute=Attribute.PARSER)(command=ori_command, context=self.__context)
                except Exception as e:
                    print(e)
                    sys.exit()
                compiler_type = self.__build_graph.read_attribute(node=work_node, attribute=Attribute.COMPILER)
                cwd = self.__build_graph.read_attribute(node=work_node, attribute=Attribute.CWD)
                compiler, options = parser.parse_command()
                ret = self.__replace_IO_options(parser=parser, dst_md5=work_node)
                if not ret:
                    self.__finished_queue.put(work_node)
                    self.__build_statistics.inc_skiped_targets()
                    logging.warning("The work node %s lacks of all input file" % work_node)
                    continue
                logging.debug("submit %s", " ".join(ori_command))
                self.__build_pool.submit(job=(work_node, cwd, compiler_type, parser))
            
            self.__build_pool.wait_for_all_done()
            self.__build_statistics.print_progress_bar()
            self.__build_statistics.export_build_statistics()

        print()
        
        for node in self.__build_graph.get_root_nodes():
            mpl_path = os.path.join(self.__context.TOP_DIR, node + self.__context.MAPLE_EXT)
            if os.path.exists(os.path.join(self.__context.MAPLE_DIR, node + self.__context.MAPLE_EXT)):
                shutil.copy(os.path.join(self.__context.MAPLE_DIR, node + self.__context.MAPLE_EXT), mpl_path)
        
        self.release_build_env()
        return self.__build_statistics.fail_targets()
    
